package com.ftl.search.service.impl;

import com.ftl.search.common.enums.ConditionType;
import com.ftl.search.model.FieldDefinition;
import com.ftl.search.model.QueryDefinition;
import com.ftl.search.model.dto.BaseSearchDTO;
import com.ftl.search.service.BaseSearchBuildService;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.MultiSearchRequest;
import org.elasticsearch.action.search.MultiSearchResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.*;

@Slf4j
@Service
public class BaseSearchBuildServiceImpl implements BaseSearchBuildService {

    private final int MAX_EXPANSIONS = 1000;

    @Resource
    private RestHighLevelClient client;

    @Override
    public List<Map<String, Object>> queryForEntity(String indexName, Map<String, List<FieldDefinition>> keywords, List<String> authClients, int size) {
        SearchRequest request = buildQuery(indexName, keywords, authClients, size);
        SearchResponse response;
        try {
            response = client.search(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace();
            return new ArrayList<>();
        }
        org.elasticsearch.search.SearchHit[] hits = response.getHits().getHits();
        List<Map<String, Object>> result = new ArrayList<>();
        for (org.elasticsearch.search.SearchHit hit : hits) {
            result.add(hit.getSourceAsMap());
        }
        return result;
    }

    private void addDict(Map<String, LinkedList<String>> dict, String key, String value) {
        if (!dict.containsKey(key)) {
            LinkedList<String> sub = new LinkedList<>();
            sub.add(value);
            dict.put(key, sub);
        } else {
            dict.get(key).add(value);
        }
    }

    private String getKeyFormDict(Map<String, LinkedList<String>> dict, String key) {
        if (!dict.containsKey(key) || dict.get(key).size() == 0) {
            return UUID.randomUUID().toString();
        }
        String nextKey = dict.get(key).getFirst();
        dict.get(key).removeFirst();
        return nextKey;
    }

    @Override
    public Map<String, List<Map<String, Object>>> queryForEntities(List<BaseSearchDTO> searches, List<String> authClients) {
        Map<String, List<Map<String, Object>>> result = new HashMap<>();
        Map<String, LinkedList<String>> dict = new HashMap<>();
        MultiSearchRequest multiSearchRequest = new MultiSearchRequest();
        for (BaseSearchDTO dto : searches) {
            multiSearchRequest.add(buildQuery(dto.getIndexName(), dto.getKeywords(), authClients, 3));
            result.put(dto.getKey(), new ArrayList<>());
            addDict(dict, dto.getIndexName(), dto.getKey());
        }
        MultiSearchResponse response;
        try {
            response = client.msearch(multiSearchRequest, RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace();
            return result;
        }
        for (MultiSearchResponse.Item item : response) {
            SearchResponse r = item.getResponse();
            if (Objects.isNull(r)) {
                continue;
            }
            org.elasticsearch.search.SearchHit[] hits = r.getHits().getHits();
            String key = "";
            List<Map<String, Object>> subResult = new ArrayList<>();
            for (org.elasticsearch.search.SearchHit hit : hits) {
                subResult.add(hit.getSourceAsMap());
                key = hit.getIndex();
            }
            if (dict.containsKey(key)){
                result.put(getKeyFormDict(dict, key), subResult);
            }
        }
        return result;
    }

    @Override
    public SearchRequest buildQuery(String indexName, Map<String, List<FieldDefinition>> keywords, List<String> authClients, int size) {
        SearchRequest request = new SearchRequest(indexName);
        SearchSourceBuilder builder = new SearchSourceBuilder();
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        keywords.forEach((key, definitions) -> {
            if ("must".equals(key) && !CollectionUtils.isEmpty(definitions)) {
                definitions.forEach(definition -> {
                    if (ConditionType.IN.equals(definition.getCondition())) {
                        boolQuery.must(QueryBuilders.termsQuery(definition.getField() + ".keyword", definition.getValues()));
                    } else if (ConditionType.EQUAL.equals(definition.getCondition())) {
                        boolQuery.must(QueryBuilders.termQuery(definition.getField() + ".keyword", definition.getValue()));
                    } else {
                        boolQuery.must(QueryBuilders.matchPhrasePrefixQuery(definition.getField(), definition.getValue()).maxExpansions(MAX_EXPANSIONS));
                    }
                });
            }
            if ("should".equals(key) && !CollectionUtils.isEmpty(definitions)) {
                BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
                definitions.forEach(definition -> {
                    if (ConditionType.IN.equals(definition.getCondition())) {
                        boolQueryBuilder.should(QueryBuilders.termsQuery(definition.getField() + ".keyword", definition.getValues()));
                    } else if (ConditionType.EQUAL.equals(definition.getCondition())) {
                        boolQueryBuilder.should(QueryBuilders.termQuery(definition.getField() + ".keyword", definition.getValue()));
                    } else {
                        boolQueryBuilder.should(QueryBuilders.matchPhrasePrefixQuery(definition.getField(), definition.getValue()).maxExpansions(MAX_EXPANSIONS));
                    }
                });
                boolQuery.must(boolQueryBuilder);
            }
        });
        if (!CollectionUtils.isEmpty(authClients)) {
            boolQuery.must(QueryBuilders.termsQuery("client.keyword", authClients.toArray()));
        }
        builder.query(boolQuery);
        builder.from(0);
        if (size > 0) {
            builder.size(size);
        }
        log.info("indexName: {}, Query: {}", indexName, builder);
        request.source(builder);
        return request;
    }

    @Override
    public Map<String, List<FieldDefinition>> buildKeyword(QueryDefinition definition, String value, List<String> values) {
        Map<String, List<FieldDefinition>> keyword = new HashMap<>();
        List<FieldDefinition> mustList = null;
        List<FieldDefinition> shouldList = null;
        if (!CollectionUtils.isEmpty(definition.getMust())) {
            mustList = new ArrayList<>();
            for (FieldDefinition d : definition.getMust()) {
                mustList.add(new FieldDefinition(
                        d.getField(),
                        d.getCondition(),
                        StringUtils.hasText(d.getValue()) ? d.getValue() : value,
                        CollectionUtils.isEmpty(d.getValues()) ? values : d.getValues()));
            }
        }
        if (!CollectionUtils.isEmpty(definition.getShould())) {
            shouldList = new ArrayList<>();
            for (FieldDefinition d : definition.getShould()) {
                shouldList.add(new FieldDefinition(
                        d.getField(),
                        d.getCondition(),
                        StringUtils.hasText(d.getValue()) ? d.getValue() : value,
                        CollectionUtils.isEmpty(d.getValues()) ? values : d.getValues()));
            }
        }
        keyword.put("must", mustList);
        keyword.put("should", shouldList);
        return keyword;
    }

    /**
     * 方案二:
     * 解决ES持有链接超过服务器keepalive时间的问题
     */
    @Scheduled(fixedRate = 60 * 1000 * 5)
    public void heartbeatToES() {
        try {
            boolean result = client.ping(RequestOptions.DEFAULT.toBuilder().build());
            log.info("ES链接状态: {}", result);
        } catch (Exception e) {
            log.error("请求ES异常: {}", e.getMessage());
        }
    }
}
